Skip to content

improvement(mothership): add file patch tool#3712

Merged
waleedlatif1 merged 30 commits intostagingfrom
improvement/mothership-file-patches
Mar 23, 2026
Merged

improvement(mothership): add file patch tool#3712
waleedlatif1 merged 30 commits intostagingfrom
improvement/mothership-file-patches

Conversation

@Sg312
Copy link
Collaborator

@Sg312 Sg312 commented Mar 22, 2026

Summary

Add file patch tool, add pptx capability

Type of Change

  • New feature

Testing

Manual

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link

vercel bot commented Mar 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 23, 2026 6:08pm

Request Review

@Sg312
Copy link
Collaborator Author

Sg312 commented Mar 22, 2026

@cursor review

@cursor
Copy link

cursor bot commented Mar 22, 2026

PR Summary

High Risk
High risk because it introduces server-side execution of user-provided PPTXGenJS code via a new subprocess VM and expands file-serving behavior (auto-compiling .pptx sources unless raw=1), plus a new remote-download tool that fetches external URLs into workspace storage.

Overview
Adds first-class PPTX generation/preview: workspace .pptx files can now represent PptxGenJS source (text/x-pptxgenjs) that is compiled on demand in api/files/serve (ZIP-magic check + raw=1 escape hatch) or via a new POST /api/workspaces/[id]/pptx/preview endpoint for streaming previews. The compilation runs in a new sandboxed Node subprocess (pptx-vm + pptx-worker.cjs) with IPC-brokered workspace file reads.

Updates the workspace FileViewer to render PPTX previews client-side by fetching binary content and converting slides to images with pptxviewjs (with small caching and debounced re-rendering for streaming updates), and adjusts query keys/hooks to support text/raw/binary modes.

Extends Copilot server tools: workspace_file gains a patch operation (search/replace edits with ambiguity checks) and .pptx writes/updates now persist as text/x-pptxgenjs; adds a new SSRF-hardened download_to_workspace_file tool and tightens tool permission enforcement/UX (propagate tool params, hide tool_search_tool_regex, improve display titles, and treat { success: false } tool outputs as failures).

Written by Cursor Bugbot for commit 5712ac4. Configure here.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR adds a patch operation to the workspace file tool (search/replace edits without full rewrites), full PPTX capability via a sandboxed pptx-worker.cjs subprocess, a download_to_workspace_file tool for fetching remote URLs into workspace storage, and a PptxPreview component that renders compiled presentations slide-by-slide using pptxviewjs. It also wires pptxgenjs/worker into next.config.ts and trigger.config.ts for standalone and Trigger.dev builds, and improves copilot tool-call display labels for read and _respond tools.

Most security concerns raised in earlier review rounds have been resolved:

  • RCE via new Function() → replaced with subprocess + vm.createContext null-prototype sandbox + stripped env
  • SSRF via redirect-following → handled by secureFetchWithValidation
  • Memory exhaustion → 50 MB cap enforced by secureFetchWithValidation
  • workspaceId always empty in serve route → fixed via parseWorkspaceFileKey
  • Streaming preview spawning unbounded subprocesses → fixed with 500ms debounce + AbortController
  • pptx-worker.cjs missing from standalone output → fixed in next.config.ts

Remaining items:

  • Stale slides bug in PptxPreview: when the file is updated or a different file is selected, old slides remain visible without any loading indicator until the first slide of the new render is ready, because the spinner condition is loading && slides.length === 0. A setSlides([]) reset before starting the render loop would fix this.
  • Sequential patch semantics undocumented: edits are applied to progressively-modified content, so a later edit's search string can match inside an earlier edit's replace text; this should at minimum be documented as intended behavior.
  • No compilation cache for compilePptxIfNeeded: every file serve spawns a fresh subprocess; with refetchOnWindowFocus: 'always' this adds latency on each tab switch.

Confidence Score: 4/5

  • Safe to merge after a one-line fix for stale-slides display; no security or data-integrity blockers remain.
  • The security-sensitive concerns from prior rounds (RCE, SSRF, memory exhaustion, subprocess env leakage) have all been addressed. The remaining open issue is a UX bug — stale slide images shown without a loading indicator when a file changes — which is clearly fixable with a single setSlides([]) call. The sequential-patch semantics issue is acknowledged but low-risk at AI usage volumes. No data loss, auth bypass, or production-reliability risk remains.
  • apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx (stale-slides display bug in PptxPreview)

Important Files Changed

Filename Overview
apps/sim/lib/execution/pptx-vm.ts New file implementing sandboxed PPTX generation via Node.js child process with IPC brokering. Previous RCE concerns resolved — subprocess uses stripped env (PATH only), and file access is brokered via message passing. Startup/generation timeouts and abort-signal support are correctly wired.
apps/sim/lib/execution/pptx-worker.cjs Worker runs user code inside a vm.createContext sandbox with a null-prototype object. EXECUTION_TIMEOUT_MS only covers synchronous evaluation as acknowledged; async wall-clock is bounded by the parent's 60s kill timer. No new concerns beyond those already discussed in prior review rounds.
apps/sim/lib/copilot/tools/server/files/workspace-file.ts Adds patch operation (search/replace edits) and text/x-pptxgenjs content-type handling. Duplicate-search and ambiguity guards are present per prior review. One open concern: sequential edits operate on progressively-modified content, so later edit search strings can match inside a prior edit's replacement text.
apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx Adds full PptxPreview component with streaming debounce (500ms) and AbortController cancel. Stale-slides bug: when cacheKey changes with no cached entry, old slides stay visible without any loading indicator because the loading spinner requires slides.length === 0.
apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts New tool to download remote URLs into workspace files. SSRF mitigations, redirect validation, and size limiting are all handled by secureFetchWithValidation. Filename sanitization strips path separators and control characters. No new concerns.
apps/sim/app/api/files/serve/[...path]/route.ts Adds compilePptxIfNeeded that auto-compiles PptxGenJS source on every serve request (ZIP magic check gates bypass). workspaceId is correctly extracted via parseWorkspaceFileKey (previous bug fixed). No caching of compiled output means a subprocess is spawned per serve — acceptable short-term but worth optimising.
apps/sim/app/api/workspaces/[id]/pptx/preview/route.ts New preview endpoint for streaming PPTX compilation. Auth, membership check, 512 KB code cap, and req.signal forwarding are all present and correct.
apps/sim/hooks/queries/workspace-files.ts Adds useWorkspaceFileBinary hook; both text and binary hooks share the contentFile parent key for consistent invalidation. refetchOnWindowFocus: 'always' and cache: 'no-store' added to binary hook per prior review.
apps/sim/next.config.ts Adds pptxgenjs node_modules and lib/execution/pptx-worker.cjs to outputFileTracingIncludes so standalone builds include the worker. Addresses the prior standalone-build breakage concern.
apps/sim/lib/copilot/store-utils.ts Introduces specialToolDisplay to show contextual labels for _respond and read tools instead of hiding/generic labels. VFS_DIR_TO_RESOURCE import looks correct. resolveToolDisplay signature extended with _toolCallId and params params, callers updated consistently.

Sequence Diagram

sequenceDiagram
    participant UI as PptxPreview (Browser)
    participant Serve as /api/files/serve
    participant Preview as /api/workspaces/[id]/pptx/preview
    participant VM as pptx-vm.ts
    participant Worker as pptx-worker.cjs (subprocess)
    participant Storage as Workspace Storage

    Note over UI: Non-streaming path
    UI->>Serve: GET /api/files/serve/[key]?context=workspace
    Serve->>Storage: Download file buffer
    Storage-->>Serve: PptxGenJS source (text/x-pptxgenjs)
    Serve->>VM: compilePptxIfNeeded(buffer, filename, workspaceId)
    VM->>Worker: spawn node pptx-worker.cjs (env=PATH only)
    Worker-->>VM: IPC ready
    VM->>Worker: IPC { type: generate, code }
    opt getFileBase64 call
        Worker->>VM: IPC { type: getFile, fileReqId, fileId }
        VM->>Storage: getWorkspaceFile + downloadWorkspaceFile
        Storage-->>VM: file buffer
        VM->>Worker: IPC { type: fileResult, data: base64 }
    end
    Worker-->>VM: IPC { type: result, data: base64 }
    VM-->>Serve: compiled PPTX Buffer
    Serve-->>UI: binary PPTX (application/vnd.openxmlformats...)
    UI->>UI: renderPptxSlides → setSlides([...images])

    Note over UI: Streaming path (debounced 500ms)
    UI->>Preview: POST /api/workspaces/[id]/pptx/preview { code }
    Preview->>VM: generatePptxFromCode(code, workspaceId, req.signal)
    VM->>Worker: spawn + generate (same IPC flow)
    Worker-->>VM: result
    VM-->>Preview: Buffer
    Preview-->>UI: binary PPTX
    UI->>UI: renderPptxSlides → setSlides([...images])
Loading

Reviews (7): Last reviewed commit: "Add cache-busting timestamp to binary fi..." | Re-trigger Greptile

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Serve route passes undefined workspaceId breaking file references
    • Both handleLocalFile and handleCloudProxy now extract workspaceId from the file key using parseWorkspaceFileKey and pass it to compilePptxIfNeeded.
  • ✅ Fixed: Server-side code execution via unsandboxed new Function
    • Replaced new Function() with vm.createContext() which creates an isolated context exposing only pptx and getFileBase64, blocking access to process and other Node.js globals.

Create PR

Or push these changes by commenting:

@cursor push cccb0dcbe3
Preview (cccb0dcbe3)
diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts
--- a/apps/sim/app/api/files/serve/[...path]/route.ts
+++ b/apps/sim/app/api/files/serve/[...path]/route.ts
@@ -6,6 +6,7 @@
 import { generatePptxFromCode } from '@/lib/copilot/tools/server/files/workspace-file'
 import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads'
 import type { StorageContext } from '@/lib/uploads/config'
+import { parseWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
 import { downloadFile } from '@/lib/uploads/core/storage-service'
 import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
 import { verifyFileAccess } from '@/app/api/files/authorization'
@@ -138,10 +139,11 @@
     const rawBuffer = await readFile(filePath)
     const segment = filename.split('/').pop() || filename
     const displayName = stripStorageKeyPrefix(segment)
+    const workspaceId = parseWorkspaceFileKey(filename)
     const { buffer: fileBuffer, contentType } = await compilePptxIfNeeded(
       rawBuffer,
       displayName,
-      undefined,
+      workspaceId || undefined,
       raw
     )
 
@@ -202,10 +204,11 @@
 
     const segment = cloudKey.split('/').pop() || 'download'
     const displayName = stripStorageKeyPrefix(segment)
+    const workspaceId = parseWorkspaceFileKey(cloudKey)
     const { buffer: fileBuffer, contentType } = await compilePptxIfNeeded(
       rawBuffer,
       displayName,
-      undefined,
+      workspaceId || undefined,
       raw
     )
 

diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts
--- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts
+++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts
@@ -1,3 +1,4 @@
+import vm from 'node:vm'
 import { createLogger } from '@sim/logger'
 import PptxGenJS from 'pptxgenjs'
 import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
@@ -36,8 +37,19 @@
     return `data:${mime};base64,${buffer.toString('base64')}`
   }
 
-  const fn = new Function('pptx', 'getFileBase64', `return (async () => { ${code} })()`)
-  await fn(pptx, getFileBase64)
+  const sandbox = {
+    pptx,
+    getFileBase64,
+    __result: null as unknown,
+  }
+
+  vm.createContext(sandbox)
+
+  const wrappedCode = `(async () => { ${code} })()`
+  const script = new vm.Script(`__result = ${wrappedCode}`, { filename: 'pptx-code.js' })
+  script.runInContext(sandbox)
+  await sandbox.__result
+
   const output = await pptx.write({ outputType: 'nodebuffer' })
   return output as Buffer
 }

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

- generatePptxFromCode now accepts an optional AbortSignal; when the
  signal fires (e.g. client disconnects mid-stream), done() is called
  which clears timers and kills the subprocess immediately rather than
  waiting for the 60s timeout
- preview route passes req.signal so client-side AbortController.abort()
  (from the streaming debounce cleanup) propagates all the way to the
  worker process
- Correct misleading comment in pptx-worker.cjs and pptx-vm.ts:
  vm.createContext is NOT a sandbox when non-primitives are in scope;
  the real security boundary is the subprocess + minimal env
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator

@greptile

@waleedlatif1
Copy link
Collaborator

@cursor review

@waleedlatif1
Copy link
Collaborator

@greptile

@waleedlatif1
Copy link
Collaborator

@cursor review

@waleedlatif1
Copy link
Collaborator

@greptile

@waleedlatif1
Copy link
Collaborator

@cursor review

@waleedlatif1
Copy link
Collaborator

@greptile

@waleedlatif1
Copy link
Collaborator

@cursor review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@waleedlatif1 waleedlatif1 merged commit 44ceed4 into staging Mar 23, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants